package org.infodavid.common.impl.lang;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.converters.reflection.AbstractReflectionConverter;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;

/**
 * The Class ObjectConverter.
 */
@SuppressWarnings("rawtypes")
public class ObjectConverter extends AbstractReflectionConverter {

	/** The logger. */
	private static final Log LOGGER = LogFactory.getLog(ObjectConverter.class);

	/** The fields . */
	private final transient Map<String,Field> fields = new HashMap<String,Field>();

	/** The fields to skip. */
	private final transient Set<String> skippedFields = new HashSet<String>();

	/** The type . */
	private Class type = null;

	/**
	 * Constructor .
	 * @param xStream XStream
	 * @param pType the type
	 */
	public ObjectConverter(final XStream xStream, final Class pType) {
		super(xStream.getMapper(), xStream.getReflectionProvider());

		setType(pType);
		registerFields();
	}

	/**
	 * Instantiate New Instance .
	 * @param reader the reader
	 * @param context the context
	 * @return the object
	 */
	@Override
	protected Object instantiateNewInstance(final HierarchicalStreamReader reader,
	    final UnmarshallingContext context) {
		return instantiateNewInstance();
	}

	/*
	 * (non-Javadoc)
	 * @see com.thoughtworks.xstream.converters.reflection.AbstractReflectionConverter
	 * #marshallField(com.thoughtworks.xstream.converters.MarshallingContext, java.lang.Object,
	 * java.lang.reflect.Field)
	 */
	@Override
	protected void marshallField(final MarshallingContext context, final Object newObj,
	    final Field field) {
		if (skippedFields.contains(field.getName())) {
			/*
			 * LOGGER.debug("Skipping field " + field.getName() + " of class " + getType().getName());
			 */
		}
		else {
			super.marshallField(context, newObj, field);
		}
	}

	/**
	 * Instantiate New Instance .
	 * @return the object
	 */
	@SuppressWarnings("unchecked")
	protected Object instantiateNewInstance() {
		Object result = null;

		try {
			final Constructor constructor = type.getConstructor();

			constructor.setAccessible(true);
			result = constructor.newInstance();
		}
		catch (final Exception e) {
			LOGGER.error("Failed to instantiate new instance for type: " + type);
		}

		return result;
	}

	/**
	 * Register the fields.
	 */
	public final void registerFields() {
		fields.clear();

		final Class clazz = instantiateNewInstance().getClass();
		Class currentClazz = clazz;
		Field[] values;

		do {
			values = currentClazz.getDeclaredFields();

			if (values != null) {
				for (final Field field : values) {
					field.setAccessible(true);
					fields.put(field.getName().toLowerCase(), field);
				}
			}

			currentClazz = currentClazz.getSuperclass();
		}
		while (currentClazz != null && clazz != currentClazz);
	}

	/**
	 * Do Unmarshal .
	 * @param data the object
	 * @param reader the reader
	 * @param context the context
	 * @return the object
	 */
	@Override
	public Object doUnmarshal(final Object data, final HierarchicalStreamReader reader,
	    final UnmarshallingContext context) {
		if (data == null) {
			LOGGER.debug("data == null");
		}
		else {
			final int attributeCount = reader.getAttributeCount();

			for (int i = 0; i < attributeCount; i++) {
				setFieldValue(data, reader, context, reader.getAttributeName(i), reader.getAttribute(i));
			}

			Field field;

			while (reader.hasMoreChildren()) {
				reader.moveDown();
				field = fields.get(reader.getNodeName().toLowerCase());

				if (field == null) {
					/*
					 * if (LOGGER.isDebugEnabled()) { LOGGER.debug("Data skipped: " + reader.getNodeName() +
					 * " = " + reader.getValue()); }
					 */
				}
				else {
					/*
					 * if (LOGGER.isDebugEnabled()) { LOGGER.debug("Setting field: " + reader.getNodeName());
					 * }
					 */

					Object fieldValue = null;

					try {
						fieldValue = field.get(data);

						if (fieldValue == null) {
							if (field.getType().isInterface()) {
								field.set(data, super.unmarshallField(context, reader, field.getType(), field));
							}
							else {
								field.set(data, unmarshallField(context, reader, field.getType(), field));
							}
						}
						else {
							field.set(data, unmarshallField(context, reader, fieldValue.getClass(), field));
						}
					}
					catch (final Exception e) {
						LOGGER.error("Failed to set value for field: " + reader.getNodeName() + " of type "
						    + type, e);
					}
				}

				reader.moveUp();
			}
		}

		return data;
	}

	/**
	 * Set Field Value .
	 * @param data the object
	 * @param reader the reader
	 * @param context the context
	 * @param fieldName the name of the field
	 * @param fieldValue the value of the field
	 */
	protected void setFieldValue(final Object data, final HierarchicalStreamReader reader,
	    final UnmarshallingContext context, final String fieldName, final String fieldValue) {
		final Field field = fields.get(fieldName.toLowerCase());

		if (field == null) {
			/*
			 * if (LOGGER.isDebugEnabled()) { LOGGER.debug("Data skipped: " + fieldName + " = " +
			 * fieldValue); }
			 */
		}
		else {
			/*
			 * if (LOGGER.isDebugEnabled()) { LOGGER.debug("Setting field: " + fieldName); }
			 */

			try {
				field.set(data, getFieldValue(reader, context, field, fieldValue));
			}
			catch (final Exception e) {
				LOGGER.error("Failed to set value for field: " + fieldName, e);
			}
		}
	}

	/**
	 * Get Field Value .
	 * @param reader the reader
	 * @param context the context
	 * @param field the object of the field
	 * @param fieldValue the value of the field
	 * @return the value
	 * @throws NoSuchMethodException the no such method exception
	 * @throws InstantiationException the instantiation exception
	 * @throws IllegalAccessException the illegal access exception
	 * @throws InvocationTargetException the invocation target exception
	 */
	@SuppressWarnings("unchecked")
	protected Object getFieldValue(final HierarchicalStreamReader reader,
	    final UnmarshallingContext context, final Field field, final String fieldValue)
	    throws NoSuchMethodException, InstantiationException, IllegalAccessException,
	    InvocationTargetException {
		Object value = null;
		final Class fieldType = field.getType();

		if (Timestamp.class.isAssignableFrom(fieldType)) {
			value = Timestamp.valueOf(fieldValue);
		}
		else if (long.class.isAssignableFrom(fieldType)) {
			value = Long.valueOf(fieldValue);
		}
		else if (double.class.isAssignableFrom(fieldType)) {
			value = Double.valueOf(fieldValue);
		}
		else if (float.class.isAssignableFrom(fieldType)) {
			value = Float.valueOf(fieldValue);
		}
		else if (int.class.isAssignableFrom(fieldType)) {
			value = Integer.valueOf(fieldValue);
		}
		else if (byte.class.isAssignableFrom(fieldType)) {
			value = Byte.valueOf(fieldValue);
		}
		else {
			final Constructor constructor = fieldType.getConstructor(String.class);

			value = constructor.newInstance(fieldValue);
		}

		return value;
	}

	/**
	 * Can Convert .
	 * @param value the class
	 * @return the boolean
	 */
	public boolean canConvert(final Class value) {
		return type == value;
	}

	/**
	 * Gets the type.
	 * @return the type
	 */
	public final Class getType() {
		return type;
	}

	/**
	 * Sets the type.
	 * @param value the type to set
	 */
	public final void setType(final Class value) {
		type = value;
	}

	/**
	 * Gets the fields.
	 * @return the fields
	 */
	public Map<String,Field> getFields() {
		return fields;
	}

	/**
	 * Gets the skipped fields.
	 * @return the skipped fields
	 */
	public Set<String> getSkippedFields() {
		return skippedFields;
	}
}
